#include "LineBuffer.hpp"

#include "SharedUtil.hpp"
#include "ElapsedTimer.hpp"

#include <QLineF>
#include <QtDebug>

class LineBuffer::LineBufferPrivate
{
public:

  enum IntersectionType {
    TIP=0,
    FIP=1,
    PFIP=3,
    NFIP=5
  };

  struct IntersectionTypes{
    IntersectionType type1;
    IntersectionType type2;

    IntersectionTypes() {
      type1 = type2 = TIP;
    }
  };

  LineBufferPrivate( LineBuffer* me ) : m_self( me ) {
  }

  QList<QPolygonF> getOffset( const QPolygonF& spine, float distance ) {
    if( spine.count() < 2 ) {
      return QList<QPolygonF>();
    }

    QList<QPolygonF> uoCurves = getUntrimmedOffsetCurves( spine, distance );
    QList<QPolygonF> finalOffset = applyClipping( spine, uoCurves, distance );

    return finalOffset;
  }

  /// Untrimmed offset curves
  QList<QPolygonF> getUntrimmedOffsetCurves( const QPolygonF& spine, float distance ) {
    QList<QPolygonF> result;
    result << untrimmedOffsetCurve( spine, distance, 1 );
    result << untrimmedOffsetCurve( spine, distance,-1 );

    return result;
  }

  /// Clipping offset curves
  QList<QPolygonF> applyClipping( const QPolygonF& spine, const QList<QPolygonF>& uoCurves, float distance ) {
    QList<QPolygonF> result;
    const QPolygonF& pline1 = uoCurves.first();
    const QPolygonF& pline2 = uoCurves.last();

    QList<QPolygonF> intersectPoints1;
    QList<QPolygonF> intersectPoints2;
    SharedUtil::getIntersectPoints( pline1, pline2, intersectPoints1, intersectPoints2 );

    intersectPoints1 = joinIntersectPoints( intersectPoints1, SharedUtil::getSelfIntersectPoints( pline1 ) );
    intersectPoints2 = joinIntersectPoints( intersectPoints2, SharedUtil::getSelfIntersectPoints( pline2 ) );

    SharedUtil::sortIntersectPoints( pline1, intersectPoints1 );
    SharedUtil::sortIntersectPoints( pline2, intersectPoints2 );

    result << dualClipping( spine, distance, pline1, intersectPoints1 );
    //result << dualClipping( spine, distance, pline2, intersectPoints2 );

    result = gccpClipping( spine, result, distance );

    return result;
  }

  QList<QPolygonF> dualClipping( const QPolygonF& spine, float distance, const QPolygonF& polyline, const QList<QPolygonF>& intersectPoints ) {
    QList<QPolygonF> result;
    int ptsCount = intersectPointsCount( intersectPoints );
    if( Q_UNLIKELY( !ptsCount ) ) {
      result << polyline;
    } else {
      QLineF line1(polyline.at(0), polyline.at(1));
      QLineF line2(polyline.at(polyline.count()-2), polyline.at(polyline.count()-1));

      QList<QPolygonF> subPolylines = splitPolyline( polyline, intersectPoints );
      Q_FOREACH( const QPolygonF& subPolyline, subPolylines ) {
        QPolygonF subIntersectPts = SharedUtil::getIntersectPoints( subPolyline, spine );
        if( subIntersectPts.isEmpty() ) {
          result << subPolyline;
        } else {
          bool bFlag = true;
          QPolygonF tempPts;
          Q_FOREACH( const QPointF& pt, subIntersectPts ) {
            if( !SharedUtil::isPointOnLine( pt, line1 ) || !SharedUtil::isPointOnLine( pt, line2 ) ) {
              bFlag = false;
            } else {
              tempPts << pt;
            }
          }
          if( bFlag ) {
            QList<QPolygonF> tempPolys = QList<QPolygonF>() << subPolyline;
            Q_FOREACH( const QPointF& pt, tempPts ) {
              QList<QPolygonF> tempPolys1;
              Q_FOREACH( const QPolygonF& tempPoly, tempPolys ) {
                tempPolys1 << SharedUtil::clippingByCircle( pt, distance, tempPoly );
              }
              tempPolys = tempPolys1;
            }
            result << tempPolys;
          }
        }
      }
    }

    return result;
  }

  /*
  General closest point pair clipping

  For all curves pi in array.
  Let d equal the offset distance, calculate the GCPPS from pline to pi. For each GCPP, construct a circle which center is the general closest point on
  pline. The pi can be divided into several segments by these circles. Preserve the segments outside of the circles and add them to Array.
  */

  QList<QPolygonF> gccpClipping( const QPolygonF& spine, const QList<QPolygonF>& polylines, float distance ) {
    QList<QPolygonF> result = polylines;
    //Q_FOREACH( const QPointF& pt, spine ) {
    //  QList<QPolygonF> temp;
    //  Q_FOREACH( const QPolygonF& polyline, result ) {
    //    temp << SharedUtil::clippingByCircle( pt, distance, polyline );
    //  }
    //  result = temp;
    //}
    return result;
  }

  QPolygonF untrimmedOffsetCurve( const QPolygonF& spine, float distance, int factor ) {
    QPointF normal = getNormal( spine.first(), spine.at( 1 ) );

    QPointF pt;
    QLineF line1;
    QLineF line2 = subLineOffset( spine.first(), spine.at( 1 ), distance, factor );
    QPolygonF polyline = QPolygonF() << line2.p1();
    for( int idx = 0; idx < spine.count()-2; ++idx ) {
      line1 = line2;
      line2 = subLineOffset( spine.at( idx+1 ), spine.at( idx+2 ), distance, factor );
      IntersectionTypes types = getIntersectionType( line1, line2, pt );
      if( types.type1 == TIP && types.type2 == TIP ) {
        polyline << pt;
      } else if( types.type1&FIP && types.type2&FIP ) {
        if( types.type1 == PFIP ) {
          polyline << pt;
        } else {
          polyline << line1.p2() << line2.p1();
        }
      } else {
        polyline << line1.p2() << line2.p1();
      }
    }
    polyline << line2.p2();

    return polyline;
  }

  QLineF subLineOffset( const QPointF& p1, const QPointF& p2, float distance, int factor ) {
    QPointF normal = getNormal( p1, p2 );
    return QLineF( p1, p2 ).translated( normal * distance * factor );
  }

  QPointF getNormal( const QPointF& p1, const QPointF& p2 ) {
    QLineF nVector = QLineF( p1, p2 ).normalVector().unitVector();
    return nVector.p2() - nVector.p1();
  }

  IntersectionTypes getIntersectionType( const QLineF& line1, const QLineF& line2, QPointF& pt ) {
    IntersectionTypes result;
    line1.intersect( line2, &pt );

    qreal length1 = QLineF(line1.p1(), pt).length();
    qreal length2 = QLineF(line1.p2(), pt).length();
    if( !qFuzzyCompare( length1+length2, line1.length() ) ) {
      if( length1 > length2 ) {
        result.type1 = PFIP;
      } else {
        result.type1 = NFIP;
      }
    }

    length1 = QLineF(line2.p1(), pt).length();
    length2 = QLineF(line2.p2(), pt).length();
    if( !qFuzzyCompare( length1+length2, line2.length() ) ) {
      if( length1 > length2 ) {
        result.type2 = PFIP;
      } else {
        result.type2 = NFIP;
      }
    }

    return result;
  }

  QList<QPolygonF> joinIntersectPoints( const QList<QPolygonF>& pts1, const QList<QPolygonF>& pts2 ) {
    if( pts1.count() != pts2.count() ) {
      return pts1;
    }

    QList<QPolygonF> result = QVector<QPolygonF>( pts1.count() ).toList();
    for( int idx = 0; idx < result.count(); ++idx ) {
      result[idx] += pts1.at( idx ) + pts2.at( idx );
    }

    return result;
  }

  int intersectPointsCount( const QList<QPolygonF>& pts ) {
    int result = 0;
    Q_FOREACH( const QPolygonF& subPts, pts ) {
      result += subPts.count();
    }

    return result;
  }

  QList<QPolygonF> splitPolyline( const QPolygonF& polyline, const QList<QPolygonF>& intersectPoints ) {
    QList<QPolygonF> result;

    QPolygonF subLines;
    for( int idx = 0; idx < polyline.count()-1; ++idx ) {
      subLines << polyline.at( idx );
      const QPolygonF& subPts = intersectPoints.at( idx );
      for( int idy = 0; idy < subPts.count(); ++idy ) {
        subLines << subPts.at( idy );

        result << subLines;
        subLines.clear();
        subLines << subPts.at( idy );
      }
    }
    subLines << polyline.last();
    result << subLines;

    return result;
  }

  LineBuffer*         m_self;
};

LineBuffer* LineBuffer::instance()
{
  static LineBuffer* selfObject = 0;
  if( Q_UNLIKELY( !selfObject ) ) {
    selfObject = new LineBuffer();
  }

  return selfObject;
}

LineBuffer::~LineBuffer()
{
}

QList<QPolygonF> LineBuffer::getOffset( const QPolygonF& spine, float distance )
{
  return _pd->getOffset( spine, distance );
}

LineBuffer::LineBuffer()
  : _pd( new LineBufferPrivate( this ) )
{
}
